<?php
require_once 'IMap.php';

class Map extends Object implements IMap, ArrayAccess, Serializable, Iterator, Countable {
	const STANDARD     = 0;
	const NO_VALUES    = 1;
	const EMPTY_VALUES = 2;

	// Recursive? mapping
	const KEYS   =  01;
	const VALUES =  02;
	const BOTH   =  03;

	const RECURS = 010;

	// Different
	const VOID = 01;
	const DUPLICATE = 02;

	public function __construct() {
		$argv = func_get_args();
		$argc = func_num_args();
		if ($argc) {
			$a0   = $argv[0];
			$this->atom = ($argc === 1 && ($a0 instanceof self || is_array($a0))) ?
				$this->toAtom($a0) : $argv;
		} else {
			$this->atom = array();
		}
	}

	/**
	 * This lib konquers all global vars
	 * @param bool $full
	 */
	public static function found ($full = true) { // Основаться
		foreach ($GLOBALS as $k => $v) {
			if ($full || $k != 'GLOBALS') {
				$GLOBALS[$k] = o($v);
			}
		}
	}

	/**
	 * @argv [int $start + int $num + mixed $value] (similar to array_fill)
	 * @argv [array|Map $keys + mixed $value] (similar to array_fill_keys)
	 * @return Map
	 */
	public static function createFill () {
		$argv = func_get_args();
		$argc = func_num_args();
		if ($argc == 3) {
			$start = $argv[0];
			$num   = $argv[1];
			$value = $argv[2];
			return self::create(array_fill($start, $num, $value));
		} else if ($argc == 2) {
			$keys  = array();
			$value = $argv[1];
			foreach ($argv[0] as $key) {
				$keys[] = o($key);
			}
			return self::create(array_fill_keys($keys, $value));
		} else {
			throw new OOException ('Wrong parameters count');
		}
	}

	/**
	 * @return Map
	 */
	public static function create () {
		$argv = func_get_args();
		$argc = func_num_args();
		return new self ($argc == 1 ? $argv[0] : $argv);
	}

	// Core methods

	/**
	 * @param mixed $key = null
	 * @param mixed $value
	 * @return Map
	 */
	public function put () {
		$argv = func_get_args();
		$argc = func_num_args();
		if ($argc == 2) {
			$value = $argv[1];
			$key   = $this->toAtom($argv[0]);
		} else if ($argc == 1) {
			$value = $argv[0];
			$key   = null;
		} else {
			throw new OOException ('Wrong parameters count');
		}

		$key === null ? $this->atom[] = $value : $this->atom[$key] = $value;
		return $this;
	}

	/**
	 * @param mixed $key
	 * @return Map
	 */
	public function remove ($key) {
		$key = $this->toAtom($key);

		unset($this->atom[$key]);
		return $this;
	}

	/**
	 * @param mixed $key
	 * @return Map
	 */
	public function get ($key) {
		$key = $this->toAtom($key);
		$this->atom[$key] = $this->toObject($this->atom[$key]);
		return $this->atom[$key];
	}

	/**
	 * Returns length of array
	 * @param bool $recursive = false
	 * @return Number
	 */
	public function length ($recursive = false) {
		return new Number (
			$recursive ? $this->countRecursive($this->atom) : count($this->atom)
		);
	}

	/**
	 * @param mixed $a
	 * @return bool
	 */
	public function equals ($a) {
		return ($a instanceof self && $a->getAtom() === $this->getAtom());
	}

	/**
	 * STANDARD checks if array is empty
	 * NO_VALUES checks if array contents is only empty arrays
	 * EMPTY_VALUES checks if array contents is only empty array or empty values
	 * @param int $mode
	 * @return bool
	 */
	public function isEmpty ($mode = self::STANDARD) {
		return $mode ? $this->isEmptyRecursive($this->atom, $mode) : empty($this->atom);
	}

	protected function isEmptyRecursive ($array, $mode) {
		$array = $this->toAtom($array);
		foreach ($array as $elem) {
			if (is_array($elem) or $elem instanceof self) {
				if (!$this->isEmptyRecursive($elem, $mode)) {
					return false;
				}
			} else {
				if ($mode == self::NO_VALUES or !empty($elem)) {
					return false;
				} // else if EMPTY_VALUES
			}
		}
		return true;
	}

	/**
	 * Searches the array for a given value and returns the corresponding key if successful
	 * @param mixed $value
	 * @return Object
	 */
	public function search ($value) {
		return $this->toObject(array_search($value, $this->atom));
	}

	/**
	 * Checks if a value exists in an array
	 * @param mixed $value
	 * @return bool
	 */
	public function hasValue ($value) {
		return in_array($value, $this->atom);
	}

	/**
	 * Checks if the given key or index exists in the array
	 * @param mixed $key
	 * @return bool
	 */
	public function hasKey ($key) {
		if (is_null($key)) {
			return false;
		}
		return array_key_exists($this->toAtom($key), $this->atom);
	}

	/**
	 * Get the part of Map, e.g Map(1, 2, 3, 4, 5, 6, 7)->range(2, 4) // Map(2, 3, 4)
	 * @param int $from
	 * @param int $to
	 * @param int $step
	 * @return Map
	 */
	public function range ($from, $to = null, $step = 1)  {
		$from = $this->toAtom($from);
		$to   = $this->toAtom($to);
		$step = $this->toAtom($step);
		$a = array();
		$length = $this->length()->getInt();
		if (!is_int($to) or $to > $length) {
			$to = $length;
		}
		for ($x = $from; $x < $to; $x += $step) {
			$a[] = $this->atom[$x];
		}
		$this->atom = $a;
		return $this;
	}

	/**
	 * Join Map into String
	 * @param string $glue
	 * @param bool $recursive
	 * @return String
	 */
	public function join ($glue = '', $recursive = false) {
		return new String(
			$recursive ? $this->joinRecursive($this->atom, $glue) : implode ($glue, $this->atom)
		);
	}

	protected function joinRecursive ($array, $glue) {
		if ($array instanceof self) {
			$array = $this->toAtom($array);
		}
		$isFirst = true;
		$string = '';
		foreach ($array as $value) {
			($isFirst) ? ($isFirst = false) : ($string .= $glue);
			$string .= (is_array($value) or $value instanceof self) ?
				$this->joinRecursive($value, $glue) : (string) $value;
		}
		return $string;
	}

	/**
	 *
	 * @param bool $preserveKeys
	 * @return Map
	 */
	public function reverse ($preserveKeys = false) {
		$this->atom = array_reverse($this->atom, $preserveKeys);
		return $this;
	}

	/**
	 * Map('1', '2', '3', '4', '5')->limit(3)  // Map('1', '2', '345')
	 * Map('1', '2', '3', '4', '5')->limit(-3) // Map('1', '2', '3')
	 * @param int|Number $limit
	 * @return Map
	 */
	public function limit ($limit) {
		$limit = $this->toAtom($limit);
		$len   = $this->length()->getInt();
		$lim   = abs($limit);
		$lastString = '';
		if ($limit >= 0) {
			if ($limit > $len) {
				return $this;
			}
			while ($len-- >= $lim) {
				if ($len == $lim) {
					end($this->atom);
					$key = key($this->atom);
				}
				$lastString = array_pop($this->atom) . $lastString;
			}
			$this->atom[$key] = $lastString;
			reset($this->atom);
		} else if ($limit > $len) {
			$this->atom = array();
		} else {
			while ($lim--) {
				array_pop($this->atom);
			}
		}
		return $this;
	}

	/**
	 * @return Number
	 */
	public function max () {
		$max = null;
		foreach ($this as $n) {
			if (empty($max)) {
				$max = new Number($n);
			} else if ($max->lt($n)) {
				$max->set($n);
			}
		}
		return $max;
	}

	/**
	 * @return Number
	 */
	public function min () {
		foreach ($this as $n) {
			if (empty($min)) {
				$min = new Number($n);
			} else if ($min->gt($n)) {
				$min->set($n);
			}
		}
		return $min;
	}

	/**
	 * @return Number
	 */
	public function sum () {
		$num = new Number;
		foreach ($this as $n) {
			$num->add($n);
		}
		return $num;
	}

	/**
	 * Return Map converted to query string, eg:
	 * Map('a':'b', 'c':'d')->toQuery() // a=b&c=d
	 * @return String
	 */
	public function toQuery () {
		$values = new Map;
		foreach ($this as $name => $value) {
			$values[] = o($name)->url(String::ENCODE) . "=" . $value->url(String::DECODE);
		}
		return $values->join('&');
	}

	/**
	 * Recursive mapping
	 * @param Closure $callback
	 * @param int $mode
	 * @param array $params
	 * @return Map
	 */
	public function walk ($callback, $mode = self::VALUES, array $params = array()) {
		$callback = $this->toAtom($callback);
		$this->atom = $this->arrayWalkRecurs($this->atom, $callback, $mode, $params);
		return $this;
	}

	// queue / stack
	/**
	 * Similar to array_shift | array_unshift
	 * @return Map|mixed
	 */
	public function first () {
		$args = func_get_args();
		if ($args) {
			$this->atom = array_merge($args, $this->atom);
			return $this;
		} else {
			return $this->toObject(array_shift($this->atom));
		}
	}

	/**
	 * Similar to array_pop | array_push
	 * @return Map|mixed
	 */
	public function last () {
		$args = func_get_args();
		if ($args) {
			$this->atom = array_merge($this->atom, $args);
			return $this;
		} else {
			return $this->toObject(array_pop($this->atom));
		}
	}

	/**
	 * If $mode is Closure clear element, with which closure returns false
	 * If $mode & Map::VOID — clears empty elements
	 * If $mode & Map::DUPLICATE — similar to array_unique
	 * @param Closure|int $mode
	 * @param int $flags
	 * @return Map
	 */
	public function clear ($mode = self::VOID, $flags = SORT_REGULAR) {
		$isClosure = $this->isClosure($mode);
		if ($isClosure or ($mode & self::VOID)) {
			$newArray = array();
			foreach ($this as $key => $value) {
				// Add value only if closure returns true, or if not empty
				if (
					( $isClosure &&  $mode(o($key), $value)) ||
					(!$isClosure && (
						($value instanceof Object && !$value->isEmpty()) ||
						empty($value)
					))
				) {
					if (is_numeric($key)) {
						$newArray[] = $value;
					} else {
						$newArray[$key] = $value;
					}
				}
			}
			$this->atom = $newArray;
		}
		if (!$isClosure and ($mode & self::DUPLICATE)) {
			$this->atom = array_unique($this->atom, $flags);
		}
		return $this;
	}

	/**
	 * Add objects, or merge with arrays|maps
	 * @return Map
	 */
	public function merge () {
		foreach (func_get_args() as $arg) {
			if (is_array($arg)) {
				$this->atom = array_merge($this->atom, $arg);
			} else if ($arg instanceof self) {
				$this->atom = array_merge($this->atom, $arg->getAtom());
			} else {
				$this->atom[] = $arg;
			}
		}
		return $this;
	}

	/**
	 * Exchanges all keys with their associated values in an array
	 * @return Map
	 */
	public function flip () {
		$this->atom = array_flip($this->atom);
		return $this;
	}
	/**
	 * Return all the keys of an map
	 * @param mixed $search
	 * If specified, then only keys containing these values are returned
	 * @param bool $strict
	 * As of PHP 5, this parameter determines if strict comparision (===)
	 * should be used during the search.
	 * @return Map
	 */
	public function keys ($search = null, $strict = false) {
		if (is_null($search)) {
			$this->atom = array_keys($this->atom);
		} else {
			$this->atom = array_keys($this->atom, $search, $strict);
		}
		return $this;
	}

	/**
	 * Return all the values of an array
	 * @return Map
	 */
	public function values () {
		$this->atom = array_values($this->atom);
		return $this;
	}

	/**
	 * Shuffle a Map
	 * @return Map
	 */
	public function shuffle () {
		shuffle($this->atom);
		return $this;
	}

	/**
	 * Extract a slice of the Map
	 * @param int $offset
	 * If offset is non-negative, the sequence will
	 * start at that offset in the array. If
	 * offset is negative, the sequence will
	 * start that far from the end of the array.
	 * @param int $length
	 * If length is given and is positive, then
	 * the sequence will have that many elements in it. If
	 * length is given and is negative then the
	 * sequence will stop that many elements from the end of the
	 * array. If it is omitted, then the sequence will have everything
	 * from offset up until the end of the
	 * array.
	 * @param bool $preserve_keys
	 * Note that array_slice will reorder and reset the
	 * array indices by default. You can change this behaviour by setting
	 * preserve_keys to true.
	 * @return Map
	 */
	public function slice ($offset, $length = 0, $preserve_keys = false) {
		$this->atom = array_slice($this->atom, $offset, $length, $preserve_keys);
		return $this;
	}

	/**
	 * Remove a portion of the Map and replace it with something else
	 * @param int $offset
	 * If offset is positive then the start of removed
	 * portion is at that offset from the beginning of the
	 * input array. If offset
	 * is negative then it starts that far from the end of the
	 * input array.
	 * @param int $length
	 * If length is omitted, removes everything
	 * from offset to the end of the array. If
	 * length is specified and is positive, then
	 * that many elements will be removed. If
	 * length is specified and is negative then
	 * the end of the removed portion will be that many elements from
	 * the end of the array. Tip: to remove everything from
	 * offset to the end of the array when
	 * replacement is also specified, use
	 * count($input) for
	 * length.
	 * @param mixed $replacement
	 * If replacement array is specified, then the
	 * removed elements are replaced with elements from this array.
	 * If offset and length
	 * are such that nothing is removed, then the elements from the
	 * replacement array are inserted in the place
	 * specified by the offset. Note that keys in
	 * replacement array are not preserved.
	 * If replacement is just one element it is
	 * not necessary to put array()
	 * around it, unless the element is an array itself.
	 * @return Map
	 */
	public function splice ($offset, $length = 0, $replacement = '') {
		array_splice($this->atom, $offset, $length, $replacement);
		return $this;
	}

	/**
	 * Recursivly change case
	 * @param int $mode
	 *     Map::VALUES[default] | Map::KEYS | Map::BOTH
	 * @param int $strWhat
	 *      Str::ALL[default] | Str::FIRST | Str::TITLE
	 * @param int $strMode
	 *      Str::UP[default] | Str::DOWN | Str::UP_FORCED | Str::DOWN_FORCED | Str::INVERT | Str::RANDOM
	 * @return Map
	 */
	public function changeCase ($mode = self::VALUES, $strWhat = Str::ALL, $strMode = Str::UP) {
		$this->walk('Str::changeCase', $mode, array($strWhat, $strMode));
		return $this;
	}


	/**
	 * Recursivly filter
	 * @param int $mode
	 *     Map::VALUES[default] | Map::KEYS | Map::BOTH
	 * @param int $strMode
	 *     Str::SLASHES[default] | Str::SLASHES_ADD| Str::HTML | Str::HTML_DECODE | Str::TAGS
	 * @return Map
	 */
	public function filter ($mode = self::VALUES, $strMode = Str::SLASHES) {
		$this->walk('Str::filter', $mode, array($strMode));
		return $this;
	}

	// Sort
	const BY_KEYS = 2;
	const KEYS_SAFE = 3;

	/**
	 *
	 * @param int $type
	 *    Map::STANDARD[default] | Map::BY_KEYS | Map::KEYS_SAFE
	 * @param bool $reverse
	 * @param int $flags
	 * @return Map
	 */
	public function sort ($type = self::STANDARD, $reverse = false, $flags = SORT_REGULAR) {
		switch ($type) {
			case self::STANDARD :
				$reverse ? rsort($this->atom, $flags)  : sort($this->atom, $flags);
			case self::BY_KEYS :
				$reverse ? krsort($this->atom, $flags) : ksort($this->atom, $flags);
			case self::KEYS_SAFE :
				$reverse ? arsort($this->atom, $flags) : asort($this->atom, $flags);
		}
		return $this;
	}

	/**
	 *
	 * @param Closure $callback
	 * @param int $type
	 *    Map::STANDARD[default] | Map::BY_KEYS | Map::KEYS_SAFE
	 * @return Map
	 */
	public function userSort ($callback, $type = self::STANDARD) {
		switch ($type) {
			case self::STANDARD:
				usort($this->atom, $callback);
			case self::BY_KEYS:
				uksort($this->atom, $callback);
			case self::KEYS_SAFE:
				uasort($this->atom, $callback);
		}
		return $this;
	}

	/**
	 *
	 * @param bool $caseInsensitive
	 * @return Map
	 */
	public function natSort ($caseInsensitive = false) {
		$caseInsensitive ? natcasesort($this->atom) : natsort($this->atom);
		return $this;
	}

	/**
	 * Creates a Map by using one array for keys and another for its values
	 * @param keys array
	 * Array of keys to be used. Illegal values for key will be
	 * converted to string.
	 * @return Map
	 */
	public function combine ($keys) {
		$this->atom = array_combine(
			is_array($keys) || $keys instanceof self ? $this->toAtom($keys)
			: func_get_args(), $this->atom
		);
		return $this;
	}

	// Iterator
	/**
	 * @return Object
	 */
	public function reset () {
		reset($this->atom);
		return $this->current();
	}

	public function rewind () {
		$this->reset();
	}

	/**
	 * @return bool
	 */
	public function valid() {
		$r =  $this->hasKey(key($this->atom));
		return $r;
	}

	/**
	 * @return Object|mixed
	 */
	public function end () {
		end($this->atom);
		return $this->current();
	}

	/**
	 * @return Object|mixed
	 */
	public function next () {
		return $this->getNext();
	}

	/**
	 * @return Object|mixed
	 */
	public function prev () {
		return $this->getPrev();
	}

	/**
	 * @return Object|mixed
	 */
	public function current () {
		$key = key($this->atom);
	        if ($key === null) {
        	    return null;
	        }
		$this->atom[$key] = $this->toObject($this->atom[$key]);
		return $this->atom[$key];
	}

	/**
	 * @return mixed
	 */
	public function key () {
		return key($this->atom);
	}

	/**
	 * @return Object|mixed
	 */
	public function getNext () {
		next($this->atom);
		return $this->current();
	}

	/**
	 * @return Object|mixed
	 */
	public function getPrev () {
		prev($this->atom);
		return $this->current();
	}

	/**
	 *
	 * @param null $value
	 * @param null $key
	 * @return Object|mixed
	 */
	public function getCur (&$key = null, &$value = null) {
		$value = $this->current();
		$key   = o($this->key());
		return $value;
	}

	/**
	 *
	 * @param Closure $func
	 * @return Map
	 */
	public function loop ($func) {
		if ($this->isClosure($func)) {
			while ($this->each ($k, $v)) {
				$this[$k] = $this->toObject($v);
				$result = $func($this[$k], $k);
				if (strtoupper($result) === 'BREAK') {
					break;
				}
			}
			$this->reset();
		}
		return $this;
	}

	public function each (&$key = null, &$value = null) {
		if (list($k, $v) = each($this->atom)) {
			$this->atom[$k] = $this->toObject($v);
			$key   = $this->toObject($k);
			$value = $this->atom[$k];
			return true;
		} else {
			$this->reset();
			return false;
		}
	}

	// Array Access
	public function offsetSet($key, $value) {
		return $this->put($key, $value);
	}

	public function offsetUnset($key) {
		return $this->remove($key);
	}

	public function offsetExists($key) {
		return isset($this->atom[$this->toAtom($key)]);
	}

	public function offsetGet($key) {
		return $this->get($key);
	}

	// Serializable
	public function serialize() {
		return serialize($this->atom);
	}
	public function unserialize($data) {
		$this->atom = unserialize($data);
	}

	// Countable
	public function count() {
		return count($this->atom);
	}

	// Get/set as object property
	public function __get ($key) {
		return $this->get($key);
	}

	public function __set ($key, $value) {
		return $this->put($key, $value);
	}

	protected function countRecursive ($array) {
		$count = 0;
		foreach ($array as $a) {
			$count += (is_array($a) or $a instanceof self) ?
				$this->countRecursive($a) : 1;
		}
		return $count;
	}

	protected function arrayWalkRecurs ($array, $callback, $mode = self::VALUES, $params = array()) {
		$newArray = array();
		if (!is_array($params)) {
			$params = $params instanceof self ?
				$params->toArray() : array($params);
		}
		foreach ($array as $key => $value) {
			if($mode & self::RECURS and (is_array($value) or $value instanceof self)) {
				$newValue = $value instanceof self ?
					$value->walk($callback, $mode, $params) :
					$this->arrayWalkRecurs($value, $callback, $mode, $params);
			} else {
				$p = array_merge(array($value), $params);
				$newValue = ($mode & self::VALUES) ?
					call_user_func_array($callback, $p) : $value;
			}
			$p = array_merge(array($key), $params);
			$newKey = ($mode & self::KEYS) ?
					call_user_func_array($callback, $p) : $key;
			$newArray[$newKey] = $newValue;
		}
		return $newArray;
    }
}